// Give players a Sparx.
local function SpawnSparx(mo)
	local sparx = P_SpawnMobjFromMobj(mo, 40*cos(mo.angle), 0, mo.height/2, MT_SPARX)
	sparx.angle = mo.angle
	sparx.sprite = SPR_SPRX
	
	sparx.wings = P_SpawnMobjFromMobj(sparx, 0, 0, 0, MT_SPARX_WINGS)
	sparx.wings.sprite = SPR_SPXW
	sparx.wings.target = sparx
	sparx.wings.flags2 = $ | MF2_LINKDRAW
	
	sparx.home = P_SpawnMobjFromMobj(mo, 0, 0, 0, MT_SPARX_HOME)
	sparx.home.target = mo
	sparx.home.sprite = SPR_DETN
	sparx.target = sparx.home
	
	sparx.master = mo.player
	
	return sparx
end

// Change Sparx's color according to his health.
local function SparxHealth(sparx)
	if sparx.health > 0
		sparx.flags2 = $ & ~MF2_DONTDRAW
		
		local colors = {SKINCOLOR_GREEN, SKINCOLOR_BLUE, SKINCOLOR_YELLOW}
		sparx.color = colors[sparx.health]
	else
		sparx.flags2 = $ | MF2_DONTDRAW
	end
end

// Sparx releases sparks.
local function SparxParticles(sparx)
	if sparx.health < 2 return end

	for i = 1, 3
		local particle = P_SpawnMobjFromMobj(sparx, 0, 0, sparx.height/2, MT_SPARX_PARTICLE)
		particle.sprite = SPR_SPRT
		particle.frame = P_RandomRange(0, 1)
		
		local colors = {SKINCOLOR_GREEN, SKINCOLOR_BLUE, SKINCOLOR_RED, SKINCOLOR_YELLOW, SKINCOLOR_MAGENTA, SKINCOLOR_PINK}
		particle.color = sparx.health == 3 and SKINCOLOR_YELLOW or colors[P_RandomRange(1, 6)]
		particle.fuse = 6
		
		particle.momx = P_RandomRange(-5, 5)*FRACUNIT
		particle.momy = P_RandomRange(-5, 5)*FRACUNIT
		particle.momz = P_RandomRange(-5, 5)*FRACUNIT
	end
end

// Sparx will never stand still.
// Give him the impression of inconsistent bobbing.
local function SparxBob(sparx)
	local target = sparx.home
	sparx.movedir = $ + FixedAngle(4*FRACUNIT)
	
	local x = target.x + FixedMul(FixedMul(sin(sparx.movedir), cos(sparx.movedir)), 8*FRACUNIT)
	local y = target.y + FixedMul(FixedMul(sin(sparx.movedir), sin(sparx.movedir)), 8*FRACUNIT)
	local z = target.z + FixedMul(cos(sparx.movedir), 8*FRACUNIT)
	
	P_TeleportMove(sparx, x, y, z)
end

// Gotta catch 'em all
local function SparxChaseGems(mo)
	if not mo.health return end
	
	if mo.tracer and not P_CheckSight(mo, mo.tracer)
		mo.tracer = nil
	end
	
	local pmo = mo.master.mo
	
	if not mo.tracer
		local target
		
		searchBlockmap("objects", function(ref, found)
			if not IsSpyroGem(found.type) return end
			if not P_CheckSight(ref, found) return end
			
			if found.marked return end
			if found.airborne return end
			
			if abs(ref.z - found.z) > 128*FRACUNIT return end
			if P_AproxDistance(ref.x - found.x, ref.y - found.y) > 256*FRACUNIT return end
			
			target = found
		end, pmo, pmo.x - 256*FRACUNIT, pmo.x + 256*FRACUNIT, pmo.y - 256*FRACUNIT, pmo.y + 256*FRACUNIT)
		
		if not target
			return
		else
			mo.tracer = target
		end
	end
	
	if not mo.chase
		S_StartSound(mo, sfx_spxdsh)
		mo.chase = true
	end
	
	mo.angle = R_PointToAngle2(mo.x, mo.y, mo.tracer.x, mo.tracer.y)
	HomingChase(mo, mo.tracer, 45*FRACUNIT)
	
	return mo.tracer
end

// Move Sparx's home around the player so Sparx keeps following it.
// Adds another layer of natural movement.
local function SparxHomeThinker(mo)
	local target = mo.target
	
	if not target return end
	
	local angle = mo.target.player.drawangle
	local moving = mo.target.player.speed
	local sparx = mo.target.sparx
	local x, y, z
	
	mo.nextx = $ or 0
	mo.nexty = $ or 0
	mo.nextz = $ or 0
	
	mo.idlex = $ or 0
	mo.idley = $ or 0
	
	if moving
		if not mo.side
			mo.side = (P_RandomChance(FRACUNIT/2) and -1 or 1)
		end
		
		x = -64 * cos(angle) + 32*FRACUNIT*mo.side
		y = -64 * sin(angle) + 32*FRACUNIT*mo.side
		z = target.height/2
		
		if sparx and sparx.pointingtimer
			sparx.searchtarget = nil
			sparx.pointingtimer = 0
		end
	else
		if leveltime % (2*TICRATE) == 0
			local dist = 64
			local side = (P_RandomChance(FRACUNIT/2) and -1 or 1)
			
			mo.nextx = dist * side * cos(angle + ANGLE_90)
			mo.nexty = dist * side * sin(angle + ANGLE_90)
			mo.nextz = target.height/P_RandomRange(1, 2)
		end
		
		x = mo.nextx
		y = mo.nexty
		z = mo.nextz
		
		if sparx
		and sparx.searchtarget
		and sparx.searchtarget.valid
		and sparx.pointingtimer
			local dist = 128
			local goodie = sparx.searchtarget
			local angle = R_PointToAngle2(target.x, target.y, goodie.x, goodie.y)
			
			x = dist * cos(angle)
			y = dist * sin(angle)
			z = target.height
			
			P_TeleportMove(mo, target.x + x, target.y + y, target.z + z)
			return
		end
	end
	
	P_TeleportMove(mo, target.x + x, target.y + y, target.z + z)
end

local function SparxSearch(mo)
	if not mo.master return end
	if not mo.master.mo return end
	
	local master = mo.master.mo
	local radius = 4096*FRACUNIT
	local candidate
	
	searchBlockmap("objects", function(ref, found)
		if not IsSpyroGem(found.type)
		and not (found.type == MT_SPYRO_BASKET and found.health)
			return
		end
		
		local dist = P_AproxDistance(master.x - found.x, master.y - found.y)
		local dist2 = candidate and P_AproxDistance(master.x - candidate.x, master.y - candidate.y) or 0
		
		if not candidate
		or dist < dist2
			candidate = found
		end
	end, master, master.x - radius, master.x + radius, master.y - radius, master.y + radius)
	
	if candidate
		mo.searchtarget = candidate
		mo.pointingtimer = 3*TICRATE
		
		S_StartSound(mo, sfx_spxdsh)
	end
end

local function ForceSparxTeleportation(mo)
	if not mo.master return end
	if not mo.master.mo return end
	
	local master = mo.master.mo
	
	if abs(R_PointToDist2(0, 0, R_PointToDist2(0, 0, mo.x - master.x, mo.y - master.y), mo.z - master.z)) >= 1024*FRACUNIT
		return true
	end
	
	local sec = master.subsector.sector
	
	for fof in sec.ffloors()
		if not HeightCheck(master.z, master.z + master.height, fof.bottomheight, fof.topheight) continue end
			
		if P_FindSpecialLineFromTag(805, fof.sector.tag) > 0
			return true
		end
	end
end

// What will Sparx do in this tic?
local function SparxMovement(sparx)
	if sparx.master
	and sparx.master.valid
	and (sparx.master.cmd.buttons & BT_TOSSFLAG)
	and not sparx.pointingtimer
		SparxSearch(sparx)
	end
	
	-- Move Sparx's home around.
	SparxHomeThinker(sparx.home)
	
	if ForceSparxTeleportation(sparx)
		P_TeleportMove(sparx, sparx.home.x, sparx.home.y, sparx.home.z)
	end
	
	local speed = max(18, FixedInt(sparx.master.speed) + 14)
	
	-- We've found something to chase. Cancel the rest of the movement.
	if SparxChaseGems(sparx) return end
	
	-- We're not chasing anything. Tell that to the rest of the script.
	sparx.chase = false
	
	-- Follow whatever we're trying to follow.
	if HomingChase(sparx, sparx.home, speed*FRACUNIT)
		if sparx.pointingtimer
			if sparx.searchtarget and sparx.searchtarget.valid
				sparx.angle = R_PointToAngle2(sparx.x, sparx.y, sparx.searchtarget.x, sparx.searchtarget.y)
			else
				sparx.searchtarget = nil
				sparx.pointingtimer = 0
			end
		else
			sparx.angle = sparx.master.drawangle
		end
		
		-- Bob!
		SparxBob(sparx)
	end
end

// Sparx makes contact with a gem.
addHook("MobjMoveCollide", function(sparx, gem)
	if not sparx.health return end
	if not IsSpyroGem(gem.type) return end
	if not HeightCheck(sparx.z, sparx.z + sparx.height, gem.z, gem.z + gem.height) return end
	if gem.airborne return end
	if sparx.tracer and gem != sparx.tracer return end
	
	sparx.tracer = nil
	
	gem.target = sparx.master.mo
	gem.flags = $ | MF_NOCLIPHEIGHT
	gem.marked = true
	gem.angle = sparx.angle
	gem.thrust = 15*FRACUNIT
	S_StartSound(gem, sfx_gembnc)
end, MT_SPARX)

// Sparx ignores boundaries when chasing.
addHook("MobjLineCollide", function(sparx, line)
	if not sparx.master or not sparx.master.valid return end
	
	local master = sparx.master.mo
	
	if sparx.chase
	or abs(R_PointToDist2(0, 0, R_PointToDist2(0, 0, sparx.x - master.x, sparx.y - master.y), sparx.z - master.z)) >= 256*FRACUNIT
		return false
	end
end, MT_SPARX)

// Sparx's wings flap!
addHook("MobjThinker", function(mo)
	if not mo.target
		P_RemoveMobj(mo)
		return
	end
	
	mo.frame = (($ + 1) % 2) | FF_TRANS20
	
	P_TeleportMove(mo, mo.target.x, mo.target.y, mo.target.z)
	mo.angle = mo.target.angle
	
	if mo.target.health
		mo.flags2 = $ & ~MF2_DONTDRAW
	else
		mo.flags2 = $ | MF2_DONTDRAW
	end
end, MT_SPARX_WINGS)

// Sparx's sparks burn out.
addHook("MobjThinker", function(mo)
	mo.frame = ($ & FF_FRAMEMASK) | ((6 - mo.fuse) * FRACUNIT)
end, MT_SPARX_PARTICLE)

// Sparx's brain
addHook("MobjThinker", function(mo)
	if not mo.master or not mo.master.valid
		P_RemoveMobj(mo)
		return
	end
	
	SparxHealth(mo)
	SparxParticles(mo)
	SparxMovement(mo)
	
	mo.pointingtimer = $ and ($ - 1) or 0
	
	if not mo.pointingtimer
		mo.searchtarget = nil
	end
end, MT_SPARX)

// Should we have a Sparx?
addHook("PlayerThink", function(p)
	if not (maptol & TOL_SPYRO) return end
	if p.spectator or not p.mo return end
	if p.bot return end
	
	if not p.mo.sparx
		p.mo.sparx = SpawnSparx(p.mo)
	end
end)

// Spyro damage.
addHook("ShouldDamage", function(t, i, s, a, d)
	if not (maptol & TOL_SPYRO) return end
	
	if not t.player return end
	if t.player.bot return end
	if (i and i.player) or (s and s.player) return false end
	if t.player.pflags & PF_GODMODE return false end
	if t.player.powers[pw_flashing] return end
	if not t.sparx.health return end
	
	if (d & DMG_DEATHMASK)
		P_KillMobj(t.sparx)
		return
	end
	
	if t.player.elemtimer and d <= DMG_FIRE
		return false
	end
	
	t.sparx.health = $ - 1
	P_DoPlayerPain(t.player)
	S_StartSound(t, sfx_shldls)
	
	return false
end, MT_PLAYER)

// We can't get hurt
addHook("ShouldDamage", do
	return false
end, MT_SPARX)

addHook("PlayerThink", function(p)
	if not p.mo return end
	if not p.mo.sparx return end
	
	p.prevlives = $ or p.lives
	
	if p.prevlives < p.lives
		p.mo.sparx.health = 3
		p.prevlives = p.lives
	elseif p.prevlives > p.lives
		p.prevlives = p.lives
	end
end)